﻿using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;

namespace GraphDrawing.Graph.Model
{
    /// <summary>
    /// Модель графа.
    /// </summary>
    public class ZOrderModel
    {
        /// <summary>
        /// Индекс для быстрого доступа к месту блока в Z-последовательности.
        /// </summary>
        readonly Dictionary<object, LinkedListNode<object>> zOrderIndex = new Dictionary<object, LinkedListNode<object>>();

        /// <summary>
        /// Z-порядок визуальных элементов при рисовании.
        /// Чем ближе к началу списка находится элемент, тем позже он будет отрисован.
        /// С помощью Z-порядка реализуется наложение визуальных элементов.
        /// </summary>
        readonly LinkedList<object> zOrderedObjects = new LinkedList<object>();

        /// <summary>
        /// Множество блоков, которые выбраны в данный момент.
        /// </summary>
        readonly HashSet<BlockModel> selectedBlocks = new HashSet<BlockModel>();

        /// <summary>
        /// Список соединений в модели.
        /// Соединение описывает источник и цель.
        /// </summary>
        readonly List<ConnectionModel> connections = new List<ConnectionModel>();

        /// <summary>
        /// Выбранная соединительная линия.
        /// </summary>
        ConnectionModel selectedConnection;

        /// <summary>
        /// Тип соединительных линий.
        /// </summary>
        const LineShapeType connectorsShape = LineShapeType.BEZIER;

        public ZOrderModel()
        {
        }

        /// <summary>
        /// Добавляет блок в модель в определённую позицию.
        /// </summary>
        /// <param name="model">Модель блока.</param>
        public void AddBlock(BlockModel model)
        {
            zOrderIndex.Add(model, zOrderedObjects.AddLast(model));
        }

        /// <summary>
        /// Удаляет блок из модели.
        /// </summary>
        /// <param name="block">Удаляемый блок.</param>
        public void RemoveBlock(BlockModel block)
        {
            var node = zOrderIndex[block];
            zOrderedObjects.Remove(node);
            zOrderIndex.Remove(block);
            connections.RemoveAll(c => c.From.Parent == block || c.To.Parent == block);
        }

        /// <summary>
        /// Перемещает элемент выше в Z-порядке.
        /// </summary>
        public void BringToFront(BlockModel block)
        {
            var blockConnections = connections.Where(model => model.From.Parent == block || model.To.Parent == block).ToArray();
            foreach (var connection in blockConnections)
            {
                var node = zOrderIndex[connection];
                zOrderedObjects.Remove(node);
                zOrderedObjects.AddLast(node);
            }
            var bnode = zOrderIndex[block];
            zOrderedObjects.Remove(bnode);
            zOrderedObjects.AddLast(bnode);
        }

        /// <summary>
        /// Возвращает перечисление доступных элементов в порядке, определяемым флагом <see cref="fromNearestToFurthest"/>.
        /// </summary>
        /// <param name="fromNearestToFurthest">Флаг, равный true, если блоки возвращаются в порядке от самого ближнего к самому дальнему, и false, если от дальнего к ближнему.</param>
        public IEnumerable<BlockModel> EnumerateBlocks(bool fromNearestToFurthest = false)
        {
            var items = zOrderedObjects.Where(o => o is BlockModel).Cast<BlockModel>();
            return fromNearestToFurthest ? items.Reverse() : items;
        }

        /// <summary>
        /// Проверяет попадание указанной точки в границы объектов модели.
        /// </summary>
        /// <param name="p">Глобальные координаты точки.</param>
        /// <param name="VisibilityCheck">Метод проверки видимости блока в области отображения. Если не указан, проверяет все элементы.</param>
        /// <returns>Возвращает результат проверки.</returns>
        public HitTestResult HitTest(Point p, Func<Rectangle, bool> VisibilityCheck = null)
        {
            var conn = connections.FirstOrDefault(model => model.HitTest(p));
            if (conn != null)
                return HitTestResult.Connection(conn);
            var blocks = EnumerateBlocks(true);
//            if (VisibilityCheck != null)
//                blocks = blocks.Where(b => VisibilityCheck(b.GetBounds()));
            foreach (var b in blocks)
            {
                var hitSlot = b.Slots.FirstOrDefault(slot => slot.HitTest(p));
                if (hitSlot != null)
                    return HitTestResult.Slot(hitSlot);
                if (b.Contains(p))
                    return HitTestResult.Block(b);
            }
            return HitTestResult.None();
        }

        /// <summary>
        /// Возвращает перечисление выделенных блоков.
        /// </summary>
        public IEnumerable<BlockModel> GetSelectedBlocks()
        {
            return selectedBlocks;
        }

        /// <summary>
        /// Делает указанный блок выделенным.
        /// </summary>
        public void Select(BlockModel block)
        {
            block.IsSelected = true;
            selectedBlocks.Add(block);
        }

        /// <summary>
        /// Делает выбранным указанное соединение.
        /// </summary>
        public void Select(ConnectionModel connection)
        {
            selectedConnection = connection;
            selectedConnection.IsSelected = true;
        }

        /// <summary>
        /// Снимает выделение с текущего блока.
        /// </summary>
        public bool Unselect(BlockModel block)
        {
            block.IsSelected = false;
            return selectedBlocks.Remove(block);
        }

        /// <summary>
        /// Снимает выделение с текущего блока.
        /// </summary>
        public void Unselect(ConnectionModel connection)
        {
            connection.IsSelected = false;
            selectedConnection = null;
        }

        /// <summary>
        /// Снимает выделение со всех блоков.
        /// </summary>
        public void ClearSelection()
        {
            foreach (var block in selectedBlocks)
                block.IsSelected = false;
            selectedBlocks.Clear();
            if (selectedConnection == null)
                return;
            selectedConnection.IsSelected = false;
            selectedConnection = null;
        }

        /// <summary>
        /// Определяет, может ли быть создано соединение между слотами (из <paramref name="from"/> в <paramref name="to"/>).
        /// </summary>
        /// <param name="from">Слот, в котором начинается связь.</param>
        /// <param name="to">Слот, в котором заканчивается связь.</param>
        public bool CanConnect(SlotModel @from, SlotModel to)
        {
            return to.CanAccept(@from);
        }

        /// <summary>
        /// Создаёт соединение между слотами блоков.
        /// </summary>
        /// <param name="from">Слот, из которого начинается связь.</param>
        /// <param name="to">Слот, в котором заканчивается связь.</param>
        public void Connect(SlotModel @from, SlotModel to)
        {
            if (!CanConnect(@from, to))
                return;
            from.Connect(to);
            connections.Add(new ConnectionModel(connectorsShape, from, to));
        }

        public IEnumerable<ConnectionModel> GetConnections()
        {
            return connections;
        }
    }
}